#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "mmu.h"
#include "proc.h"
#include "defs.h"
#include "x86.h"
#include "elf.h"

//exec系统调用模拟用户栈的空间分布图可参考第二章的图2-3
int
exec(char *path, char **argv)
{
  char *s, *last;
  int i, off;
  uint argc, sz, sp, ustack[3+MAXARG+1];
  struct elfhdr elf;
  struct inode *ip;
  struct proghdr ph;
  pde_t *pgdir, *oldpgdir;
  struct proc *curproc = myproc();  // 获取当前进程

  begin_op(); // 在执行文件系统的系统调用之前，确保有条件进行日志记录

  if((ip = namei(path)) == 0){  // 通过 `namei`打开二进制文件，返回被打开文件的inode
    end_op();
    cprintf("exec: fail\n");
    return -1;
  }
  ilock(ip);  // 锁定inode
  pgdir = 0;

  // Check ELF header
  // 检查ELF头，先通过inode读取ELF头结构体，然后检查ELF头结构体里的magic数字
  if(readi(ip, (char*)&elf, 0, sizeof(elf)) != sizeof(elf))
    goto bad;
  if(elf.magic != ELF_MAGIC)
    goto bad;

  if((pgdir = setupkvm()) == 0) //新申请了一页存新的页目录，并初始化一下新页目录，主要是映射内核页面
    goto bad;

  // Load program into memory.
  sz = 0;
  for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
    if(readi(ip, (char*)&ph, off, sizeof(ph)) != sizeof(ph)) // 读取程序段头文件结构体，首地址为ph（program header）
      goto bad;
    if(ph.type != ELF_PROG_LOAD)  // 检查程序段类型
      continue;
    if(ph.memsz < ph.filesz)  // memsz是程序段在内存中大小，通常大于等于程序段在文件系统中的大小，比如有一些零初始化的段
      goto bad;
    if(ph.vaddr + ph.memsz < ph.vaddr) // memsz不能为负数
      goto bad;
    if((sz = allocuvm(pgdir, sz, ph.vaddr + ph.memsz)) == 0) // 将程序的内存从sz=0扩容到ph.vaddr+ph.memsz 并进行映射，所以下面的loaduvm可以正常装载程序内容
                                                             // 【此处不理解为什么是ph.vaddr+ph.memsz而不是ph.memsz，为什么sz为0】？？？？
      goto bad;
    // 此时sz = ph.vaddr + ph.memsz
    if(ph.vaddr % PGSIZE != 0)  //检查vaddr是否对齐
      goto bad;
    if(loaduvm(pgdir, (char*)ph.vaddr, ip, ph.off, ph.filesz) < 0)  // 通过程序段的头，把程序内容从磁盘读取到虚拟内存中的vaddr～vaddr+filesz空间里，这段虚拟内存空间是已经映射的
      goto bad;
    //申请了memsz空间，但是只读filesz大小的内容
  }
  iunlockput(ip);  // 解锁inode
  end_op();  // 结束系统调用，确保日志提交
  ip = 0;

  // Allocate two pages at the next page boundary.
  // Make the first inaccessible.  Use the second as the user stack.
  // 在程序段的上边界PGROUNDUP(sz)处再申请两页，与程序段相接的页设置为不可访问，用来确保安全，第二个页作为用户程序栈
  sz = PGROUNDUP(sz);
  if((sz = allocuvm(pgdir, sz, sz + 2*PGSIZE)) == 0)
    goto bad;
  // 此时sz = PGROUNDUP(sz)+2*PGSIZE
  clearpteu(pgdir, (char*)(sz - 2*PGSIZE)); //去除以sz - 2*PGSIZE为首地址的页的用户访问权限，方法是把指向该物理页的页表项的PTE_U位给置0
  sp = sz; //因为是栈地址，所以是第二个页的上边界，栈往下增长，从高地址开始写到低地址

  // Push argument strings, prepare rest of stack in ustack（不是userstack的意思）.
  for(argc = 0; argv[argc]; argc++) { // 一次性把参数字符串拷贝到栈顶
    if(argc >= MAXARG)
      goto bad;
    sp = (sp - (strlen(argv[argc]) + 1)) & ~3;  // sp栈地址向下增长，【sp - (strlen(argv[argc]) + 1)】是给字符串变量及末尾的\0留位置
                                                // 与操作【& ~3】应该是用于4字节对齐，毕竟地址是4字节长度，栈寻址访问也是4字节对齐的
    if(copyout(pgdir, sp, argv[argc], strlen(argv[argc]) + 1) < 0) // 从地址argv[argc]处开始拷贝strlen(argv[argc]) + 1字节的数据到虚拟地址sp
      goto bad;
    ustack[3+argc] = sp; // 在ustack[3+argc]位置处记录上一步copyout函数刚写入栈中的字符串数据首地址
  }
  ustack[3+argc] = 0;//栈的最后一个数据处写入0

  ustack[0] = 0xffffffff;  // fake return PC  栈的第一个数据
  ustack[1] = argc;  // 栈的第二个数据：指新程序main函数的参数个数
  ustack[2] = sp - (argc+1)*4;  // argv pointer  栈的第三个数据：是指向新程序main函数第一个参数argv[0]地址的地址，即 &argv，通常也就是main函数里的二级指针形参【char** argv】
  //ustack[1]和ustack[2]相当于时main函数的两个形参

  sp -= (3+argc+1) * 4; // 再申请(3+argc+1)个4字节 的空间，用于：
  if(copyout(pgdir, sp, ustack, (3+argc+1)*4) < 0)//存ustack数组，ustack一个元素4字节，上面是存了栈上argc个字符串的首地址，外加开始处3个数据以及最后一个全0数据
    goto bad;

  // Save program name for debugging.
  for(last=s=path; *s; s++)
    if(*s == '/')
      last = s+1; //找到路径的最后一个"/"的后一个字符地址，即程序名的首地址
  safestrcpy(curproc->name, last, sizeof(curproc->name)); // 把程序名赋给curproc->name，即记录在进程结构体中用于debugging

  // Commit to the user image.
  // 只有在确认加载成功时，才能释放旧的程序映像，并换成新的映像（映像其实就是页目录，指代整个虚拟内存）
  oldpgdir = curproc->pgdir; //记录一下当前进程的页目录地址
  curproc->pgdir = pgdir;  //更新当前进程的页目录地址，换成另一个页面保存的新页目录
  curproc->sz = sz;     // 此时的sz应该是【程序段的上页边界地址+2*PGSIZE】，即 栈底地址（栈从上往下增，此处指栈最高地址）
  curproc->tf->eip = elf.entry;  // elf.entry是该elf文件对应程序的入口地址，即/init的main函数地址，将tf结构体的PC设为此栈指针，从exec系统调用返回时就会跳转至/init的main函数
  curproc->tf->esp = sp;  //保存栈指针
  switchuvm(curproc);  //切换用户进程，包括切换虚拟内存页目录表等信息
  freevm(oldpgdir);  //释放原来进程页目录表所在页面的内存
  return 0;  //exec系统调用返回0

 bad:
  if(pgdir)  //如果pgdir不为0，则证明执行过pgdir = setupkvm())，所以需要释放setupkvm函数里调用kalloc申请过的内存
    freevm(pgdir);
  if(ip){  //解锁inode、结束系统调用并确保日志已上传
    iunlockput(ip);
    end_op();
  }
  return -1;  // exec系统调用返回-1
}
